package com.yeming.tinyid.service;

import com.yeming.tinyid.dal.entity.TinyIdInfo;
import com.yeming.tinyid.dal.mapper.TinyIdInfoMapper;
import com.yeming.tinyid.util.bo.SegmentId;
import com.yeming.tinyid.util.common.CommonConstants;
import com.yeming.tinyid.util.exception.TinyidException;
import com.yeming.tinyid.util.generator.ISegmentIdService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @author yeming.gao
 * @Description: 号段服务层
 * @date 2020/7/28 17:24
 */
@Service
public class SegmentIdServiceImpl implements ISegmentIdService {

    private static final Logger LOGGER = LoggerFactory.getLogger(SegmentIdServiceImpl.class);

    @Resource
    private TinyIdInfoMapper tinyIdInfoMapper;

    /**
     * Transactional标记保证query和update使用的是同一连接
     * 事务隔离级别应该为READ_COMMITTED,Spring默认是DEFAULT(取决于底层使用的数据库，mysql的默认隔离级别为REPEATABLE_READ)
     * <p>
     * 如果是REPEATABLE_READ，那么在本次事务中循环调用tinyIdInfoDAO.queryByBizType(bizType)获取的结果是没有变化的，也就是查询不到别的事务提交的内容
     * 所以多次调用tinyIdInfoDAO.updateMaxId也就不会成功
     *
     * @param bizType 业务类型
     * @return SegmentId
     */
    @Override
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public SegmentId getNextSegmentId(String bizType) {
        // 并发或者分布式的情况下：获取nextTinyId的时候，有可能存在version冲突，需要重试
        for (int i = 0; i < CommonConstants.RETRY; i++) {
            TinyIdInfo tinyIdInfo = tinyIdInfoMapper.queryByBizType(bizType);
            if (Objects.isNull(tinyIdInfo)) {
                LOGGER.warn("bizType={}业务类型对应的id信息不存在", bizType);
                throw new TinyidException("对应的业务类型不存在");
            }
            //获取新的最大值（当前最大值+步长）；
            Long newMaxId = tinyIdInfo.getMaxId() + tinyIdInfo.getStep();
            //老的最大值
            Long oldMaxId = tinyIdInfo.getMaxId();
            //获取号段之前进行更新（这里版本号条件一定要加上，相当于一个乐观锁）
            int row = tinyIdInfoMapper.updateMaxId(tinyIdInfo.getBizType(), newMaxId, oldMaxId, tinyIdInfo.getVersion());
            if (row == 1) {
                tinyIdInfo.setMaxId(newMaxId);
                SegmentId segmentId = convert(tinyIdInfo);
                LOGGER.info("bizType={}业务类型号段获取成功，segmentId：{}", bizType, segmentId);
                return segmentId;
            } else {
                LOGGER.info("bizType={}业务类型对应的id信息存在{}条", bizType, row);
            }
        }
        throw new TinyidException("bizType=" + bizType + "业务类型号段获取成功失败");
    }

    private SegmentId convert(TinyIdInfo idInfo) {
        SegmentId segmentId = new SegmentId();
        //当前值
        long currentId = idInfo.getMaxId() - idInfo.getStep();
        segmentId.setCurrentId(new AtomicLong(currentId));
        //最新的最大值
        segmentId.setMaxId(idInfo.getMaxId());
        //余数
        segmentId.setRemainder(idInfo.getRemainder() == null ? 0 : idInfo.getRemainder());
        //每次id增量
        segmentId.setDelta(idInfo.getDelta() == null ? 1 : idInfo.getDelta());
        // 默认20%加载；例如：1+10000*20%/100=21(意思是从1增加到21的时候就开始预加载下一个号段了)
        segmentId.setLoadingId(segmentId.getCurrentId().get() + idInfo.getStep() * CommonConstants.LOADING_PERCENT / 100);
        return segmentId;
    }
}
